Skip to content

Key Gotchas

Video Summary

In the previous lesson, I shared my go-to solution for situations where our data doesn't come with a “built-in” ID: when the data is first created, I generate a unique value, and attach it to the data. Then, when I'm iterating through the data, I can use this value as the key.

You might be wondering, though: why am I going through all this trouble when there are simpler solutions available?

For example, I can use the array index as the key:

<button>
{stickers.map((sticker, index) => (
<img
key={index}
/>
))}
</button>

Or, maybe I can use an increasing counter, with stickers.length:

<button
onClick={(event) => {
const stickerData = getSticker();
const newSticker = {
...stickerData,
x: event.clientX,
y: event.clientY,
id: stickers.length
};
}}
>

Both of these solutions will work in this case, but they won't work in every case. In other scenarios, these two alternatives will lead to significant performance problems. They can even lead to baffling, hard-to-understand UI bugs.

In this video, we dig into two scenarios where both of these solutions will cause problems. First, we look at a slight variant on the “Sticker” example, where I can right-click to delete stickers:

We also look at an “Invitee List” application:

Unfortunately, it's difficult to adequately summarize the problems with these two scenarios; I highly recommend watching (or re-watching) this video to see exactly what the problems are.

To quickly summarize:

  • The problem with using the array index as the key is that we can never delete or re-order the indexes. If we remove the first item in the array, for example, React will actually delete the DOM nodes associated with the last item in the array, and will then have to do a bunch of work on all the other DOM nodes.
  • The problem with using stickers.length is that it can lead to duplicate keys, if items can be deleted.

Keys are how React uniquely identifies each DOM node. If we tell React that a given DOM node is identified by 0 or 1, React will change that specific DOM node on every render so that it matches the current UI.

Here are the Figma diagrams from the video:

Diagram from Figma showing what happens when we use array indexes as keys. The final item is deleted, and each sticker is updatedDiagram from Figma showing 6 stickers with ID 0, 1, 2, 3, 4, 4. The first sticker, the cactus, is opaque, signifying that it's been deleted

Here are the two sandboxes from the videos above, with comments explaining some things that were glossed over in the video:

Removable stickers:

Code Playground

import React from 'react';

import styles from './StickerPad.module.css';
import { getSticker } from './Stickers.data';

function StickerPad() {
const [stickers, setStickers] = React.useState(
[]
);

function addSticker(event) {
const stickerData = getSticker();
const newSticker = {
...stickerData,
x: event.clientX,
y: event.clientY,
};

const nextStickers = [...stickers, newSticker];
setStickers(nextStickers);
}

// This method removes the sticker at the specified
// index. Since we're not allowed to mutate arrays
// that are held in state, we create a copy of the
// array using the spread syntax (...), and then use
// the `splice` method to remove the sticker:
function deleteSticker(index) {
const nextStickers = [...stickers];
nextStickers.splice(index, 1);
setStickers(nextStickers);
}

return (
<>
<button
className={styles.addStickerBtn}
onClick={addSticker}
/>
{stickers.map((sticker, index) => (
<button
key={index}
className={styles.sticker}
onClick={addSticker}
// `onContextMenu` is how we listen for
// right-click events. This is part of the
// DOM, not a React-specific thing.
onContextMenu={(event) => {
event.preventDefault();
deleteSticker(index);
}}
// In order to support folks who don't use
// a pointer device, and therefore can't
// right-click, let's also allow users to
// delete stickers by focusing them and
// pressing the “Delete” key.
onKeyDown={(event) => {
if (
event.key === 'Delete' ||
event.key === 'Backspace'

Invitee List:

Code Playground

import React from 'react';

function App() {
const [invitees, setInvitees] = React.useState([
'J. Jonah Jameson',
'Mary Jane',
'Aunt May',
]);

// NOTE: This form is incomplete. It should have:
// • A <label> for each input
// • A <form> tag, with submission behaviour
//
// I'm omitting them here because they're not
// relevant to the problem at hand. Please don't
// use this markup as a template for anything real 😅

return (
<>
<h1>Invitees</h1>
<ul>
{invitees.map((item, index) => (
<li key={index}>
<input
// the `defaultValue` attribute
// allows us to initialize the input
// to a particular value, without
// binding the input to it. This
// will produce an *uncontrolled*
// input.
defaultValue={invitees[index]}
/>
<button
onClick={() => {
const nextInvitees = [...invitees];
nextInvitees.splice(index, 1);
setInvitees(nextInvitees);
}}
>
Delete
</button>
</li>
))}
</ul>
</>
);
}

export default App;
preview
console